import * as net from 'net'
import * as WebSocket from 'ws'
import { ActionType, Banner, Device, DeviceRotation, STFEaseConfig, TouchMeta } from './interfaces'
import { exec } from 'child_process'
import { readFileSync } from 'fs'
import { ADBFactory, execSafe, createImageCrop, logger, releasePorts } from './utils'
import { join } from 'path'
import { EventEmitter } from 'events'

const frame_throttle = () => {
    let last_frame: Buffer
    let timer: NodeJS.Timer
    /**
     * 超过2s没有接收到frame，执行回调
     * @param frame 
     * @param cb 
     * @returns 
     */
    const frame_timer = (frame: Buffer, cb?: () => void) => {
        last_frame = frame
        if (cb) {
            clearTimeout(timer)
            timer = setTimeout(cb, 2000)
        }
        return true
    }
    const clear = (ws: WebSocket) => {
        clearTimeout(timer)
    }
    return {
        frame_timer,
        clear,
        get_last_frame: () => last_frame
    }
}

export class StreamBuilder {
    frame_throttle = frame_throttle()
    stream: net.Socket
    private clients: Set<WebSocket>
    addClient = (ws: WebSocket) => {
        const t = this
        const send = (type: ActionType, info: any) => {
            ws.send(`${type}: ${JSON.stringify(info)}`)
        }
        ws.on('close', () => {
            t.clients.delete(ws)
            t.frame_throttle.clear(ws)
        })
        ws.on('message', (data) => {
            const [type, body] = data.toString().split(/(?<=^\d+):\s/)
            let frame = t.frame_throttle.get_last_frame()
            switch (type as ActionType) {
                case ActionType.BANNER:
                    send(ActionType.BANNER, t.banner)
                    break;
                case ActionType.DEVICE:
                    send(ActionType.DEVICE, t.device)
                    if (t.banner && frame) {
                        ws.send(frame, { binary: true })
                    }
                    break;
                case ActionType.TOUCH_META:
                    send(ActionType.TOUCH_META, t.touch_meta)
                    break;
                case ActionType.RECONNECT:
                    t.stream && t.stream.destroy()
                    t.close()
                    break;
                case ActionType.COMMAND:
                    let shell = `${t.config.adb} -s ${t.device.id} shell ${body}`
                    execSafe(shell)
                    break;
                case ActionType.TOUCH:
                    if (body && t.touch && t.touch.writable) {
                        const [_t, _i, x0, y0, p] = body.split(/\s+/)
                        if (!t.touch_meta) {
                            t.touch.write(`${body}\nc\n`)
                            return
                        }
                        switch (_t) {
                            case 'd':
                            case 'm':
                                let _x = 0
                                let _y = 0
                                let x = 0
                                let y = 0
                                switch (t.device.rotation) {
                                    case DeviceRotation.R0:
                                        _x = Number(x0) / t.banner.realWidth
                                        _y = Number(y0) / t.banner.realHeight
                                        x = Math.floor(t.touch_meta.max_x * _x)
                                        y = Math.floor(t.touch_meta.max_y * _y)
                                        break;
                                    case DeviceRotation.R90:
                                        _x = Number(x0) / t.banner.realHeight
                                        _y = Number(y0) / t.banner.realWidth
                                        y = Math.floor(t.touch_meta.max_y * _x)
                                        x = Math.floor(t.touch_meta.max_x * (1 - _y))
                                        break;
                                    case DeviceRotation.R270:
                                        break;
                                    default:
                                        // 旋转角度异常，不予解析
                                        return;
                                }
                                t.touch.write(`${_t} ${_i} ${x} ${y} ${p}\nc\n`)
                                break;
                            default:
                                t.touch.write(`${body}\nc\n`)
                        }
                    }
                    break;
            }
        })

        t.emitter.on('device:rotationchanged', function () {
            send(ActionType.DEVICE, t.device)
        })
        this.clients.add(ws)
    }
    banner: Banner = {
        version: 0,
        length: 0,
        pid: 0,
        realWidth: 0,
        realHeight: 0,
        virtualWidth: 0,
        virtualHeight: 0,
        orientation: 0,
        quirks: 0
    }
    private __init_cap__ = (port: number) => {
        const t = this
        const { frame_timer } = this.frame_throttle
        let stream = net.connect({ port })
        this.stream = stream
        let readBannerBytes = 0
        let bannerLength = 2
        let readFrameBytes = 0
        let frameBodyLength = 0
        let frameBody = Buffer.alloc(0)
        let banner = { ...this.banner }
        let device = this.device
        let rate = Math.min(20, t.config.rate) || 20
        const tryRead = () => {
            let chunk: Buffer
            while (chunk = stream.read()) {
                let i = 0, len = chunk.length
                while(i < len) {
                    if (readBannerBytes < bannerLength) {
                        switch (readBannerBytes) {
                            case 0:
                                banner.version = chunk[i]
                                break;
                            case 1:
                                banner.length = bannerLength = chunk[i]
                                if (device.is_devil) {
                                    bannerLength = 24
                                }
                                break;
                            case 2: case 3: case 4: case 5:
                                banner.pid += (chunk[i] << ((readBannerBytes - 2) << 3)) >>> 0
                                break;
                            case 6: case 7: case 8: case 9:
                                banner.realWidth += (chunk[i] << ((readBannerBytes - 6) << 3)) >>> 0
                                break;
                            case 10: case 11: case 12: case 13:
                                banner.realHeight += (chunk[i] << ((readBannerBytes - 10) << 3)) >>> 0
                                break;
                            case 14: case 15: case 16: case 17:
                                banner.virtualWidth += (chunk[i] << ((readBannerBytes - 14) << 3)) >>> 0
                                break;
                            case 18: case 19: case 20: case 21:
                                banner.virtualHeight += (chunk[i] << ((readBannerBytes - 18) << 3)) >>> 0
                                break;
                            case 22:
                                banner.orientation += chunk[i] * 90
                                break;
                            case 23:
                                banner.quirks = chunk[i]
                                break;
                        }
                        i++
                        readBannerBytes++
                        if (readBannerBytes === bannerLength && device.is_devil) {
                            let t = this
                            //javacap 需要不停发送信息才返回帧
                            let run = function run () {
                                t.stream.write(`get \r\n\r\n`)
                                t.screencap_timer = setTimeout(run, 2000 / rate)
                            }
                            run()
                        }
                    } else if (readFrameBytes < 4) {
                        frameBodyLength += (chunk[i] << (readFrameBytes << 3)) >>> 0
                        i++
                        readFrameBytes++
                    } else {
                        if (len - i >= frameBodyLength) {
                            frameBody = Buffer.concat([frameBody, chunk.slice(i, i + frameBodyLength)])

                            if (frameBody[0] !== 0xff || frameBody[1] !== 0xD8) {
                                logger.error('frameBody is not JPG!', device.id)
                                t.clients.forEach((ws) => {
                                    ws.send(`${ActionType.ERROR}: frameBody is not JPG!`)
                                })
                                t.close();
                                break;
                            } else {
                                try {
                                    if (!t.config.support_rotate || device.is_devil) {
                                        frame_timer(frameBody)
                                        t.clients.forEach((ws) => {
                                            if (ws.readyState === ws.OPEN) {
                                                ws.send(frameBody, { binary: true })
                                            }
                                        })
                                    } else if (t.imageCrop) {
                                        t.imageCrop(frameBody).then(buf => {
                                            frame_timer(buf)
                                            t.clients.forEach((ws) => {
                                                if (ws.readyState === ws.OPEN) {
                                                    ws.send(buf, { binary: true })
                                                }
                                            })
                                        }).catch(_e => {
                                            console.log(_e)
                                        })
                                        
                                    }
                                    i += frameBodyLength
                                    frameBodyLength = readFrameBytes = 0
                                    frameBody = Buffer.alloc(0)
                                } catch (e) {

                                }
                            }
                        } else {
                            frameBody = Buffer.concat([frameBody, chunk.slice(i, len)])
                            frameBodyLength -= len - i
                            readFrameBytes += len - i
                            i = len
                        }
                    }
                }
            }
        }

        stream.on('readable', tryRead);
        stream.on('error', (e: Error) => {
            logger.error('minicap error:', e)
            this.clients.forEach((ws) => {
                if (ws.readyState === ws.OPEN) {
                    ws.send(`${ActionType.ERROR}: minicap stream error!`)
                }
            })
        })
        stream.on('close', () => {
            logger.error('minicap close:', device.id)
            this.close && this.close()
        })
    }

    device: Device
    config: STFEaseConfig
    adb_factory: ADBFactory
    close?: () => void
    constructor ({
        device, port, port1, config, onClose
    }: {
        device: Device,
        /** mincap 端口 */
        port: number,
        /** minitouch 端口 */
        port1: number,
        config: STFEaseConfig,
        /** 异常关闭后 */
        onClose?: () => void
    }) {
        this.device = device
        this.config = config
        this.adb_factory = new ADBFactory(this.config.adb)
        this.clients = new Set()
        this.close = () => {
            clearTimeout(this.screencap_timer)
            releasePorts(port, port1)
            this.adb_factory.clearAdbTask(device.id)
            this.clients.forEach(ws => ws.close())
            onClose && onClose()
        }

        /** javacap 的banner解析不靠谱，直接使用adb结果 */
        const size = device.size.override || device.size.physical
        const [w, h] = size.match(/\d+/g).map(Number)
        this.banner.realWidth = w
        this.banner.realHeight = h

        if (port) {
            if (config.support_rotate && !device.is_devil) {
                this.imageCrop = createImageCrop(device)
            }
            this.__listene_rotation_change__()
            this.__init_cap__(port)
        } else {
            this.__init_screencap__()
        }
        if (port1) {
            this.__init_touch__(port1)
        }
    }
    emitter = new EventEmitter()
    imageCrop: ReturnType<typeof createImageCrop>
    private __listene_rotation_change__ = async () => {
        const t = this
        const { adb_factory, device, emitter } = t
        const rotation = await adb_factory.getRotation(device.id)
        if (rotation != device.rotation || !t.imageCrop) {
            if (rotation != device.rotation) {
                device.rotation = rotation
                emitter.emit('device:rotationchanged')
            }
            t.imageCrop = createImageCrop(device)
        }
        clearTimeout(this.screencap_timer)
        this.screencap_timer = setTimeout(this.__listene_rotation_change__, 3000)
    }
    touch: net.Socket
    touch_meta: TouchMeta = null
    private __init_touch__ = (port: number) => {
        let touch = net.connect({ port })
        let device = this.device
        touch.on('close', () => {
            logger.error('minitouch close: ', this.device.id)
        })
        this.touch_meta = null
        touch.on('data', (data) => {
            data.toString()
                .split(/[\r\n]+/)
                .filter(a => a)
                .map(line => {
                    if (line.startsWith('^')) {
                        const [max_contacts, max_x, max_y, max_pressure] = line.match(/\d+/g).map(Number)
                        this.touch_meta = {max_contacts, max_x, max_y, max_pressure}
                        logger.info('touch_meta: \t', device.id + '\t', JSON.stringify(this.touch_meta))
                    }
                })
        })
        this.touch = touch
    }
    
    screencap_timer: NodeJS.Timer
    private __init_screencap__ = () => {
        const t = this
        const { frame_timer } = t.frame_throttle
        const screen_path = join(__dirname, `../screencaps/${t.device.id}.png`)
        const run = function run () {
            const { clients, config: { adb }, device } = t
            if (clients.size > 0) {
                exec(`${adb} -s ${device.id} shell screencap -p | sed 's/\\r$//' > ${screen_path}`, function (err) {
                    if (err) {
                        logger.error(device.id, err)
                    } else {
                        const frameBody = readFileSync(screen_path)
                        frame_timer(frameBody, function () {
                            logger.error(device.id, 'screencap stop!')
                            t.close()
                        })
                        clients.forEach((ws) => {
                            if (ws.readyState === ws.OPEN) {
                                ws.send(frameBody, { binary: true })
                            }
                        })
                    }
                    t.screencap_timer = setTimeout(run, 300)
                })
            } else {
                t.screencap_timer = setTimeout(run, 3000)
            }
        }
        run()
    }
}